from erlglobals import *
from rltext import *

# Basic types of components:
#	- sensors : detect when something is happening (impact, radio signal)
#	- payload : trigger an effect of some kind (weapon, mix chemicals..)
#	- generator : generate energy from fuel
#	- battery : store power for later use

# A device will often require power to run.
# Cables can transfer a limited amount of power per turn.

# Some devices require fuel or ammunition.
# Loaders can load a limited amount per turn, but this is
# generally only an issue with machine-guns.

# Signals are passed between components as a form of
# communication. Special components can be used to
# emit, multiply, randomize, delay, and so on, signals.

# Redesign! Thought up a new system, simplifying things a bit.
#  Devices consist of components. (Links are NOT physical objects,
#  and they are directed.)
#
#  Links are encoded as properties (the value being pointer
#  values to appropriate components).
#
#  Properties are not necessarily simple hardcoded values.
#	- they can be hardcoded values
#	- they can be a dial, presenting an interface for the user to
#	  change the value given physical access
#	- they can be a rotating cycle (for load-balancing etc.)
#	- they can be a random choice (possibly weighted)
#	- they can mirror ANOTHER dial on the device (specifically a user-settable _dial_)

# self.property["trigger"]().request(100)
# self.get("trigger").request(100)
# self["trigger"].request(100)

class CannotLoadItem: pass

from erlitem import *

class HardcodedValue:
	serializedAttributes = [ "value" ]
	def __init__(self, value):
		self.value = value
	def access(self):
		return self.value
	def getValues(self):
		return [ self.value ]
	def __str__(self):
		return "set to %s" % str(self.value)
supportsSerialization( HardcodedValue )

class CyclicValue:
	serializedAttributes = [ "values", "idx" ]
	def __init__(self, values):
		self.values = values
		self.idx = 0
	def access(self):
		oldIdx = self.idx
		self.idx = (1 + self.idx) % len(self.values)
		return self.values[oldIdx]
	def getValues(self):
		return self.values
	def __str__(self):
#		return "cycle between %s" % [ str(v) for v in self.values ]
		return "cycle between %s" % ( makeEnglishList( [ str(v) for v in self.values ] ), )
supportsSerialization( CyclicValue )

class RandomValue:
	serializedAttributes = [ "values" ]
	def __init__(self, values):
		self.values = values # list of items.
	def access(self):
		import random
		return random.choice( self.values )
	def getValues(self):
		return self.values
	def __str__(self):
		from rltext import makeEnglishList
		return "random draw between %s" % ( makeEnglishList( [ str(v) for v in self.values ] ), )
#		return "chosen between %s, initially %s" % ( makeEnglishList( [ str(v) for v in self.values ] ), str(self.value) )
supportsSerialization( RandomValue )

class DialValue:
	serializedAttributes = [ "value", "values" ]
	def __init__(self, values):
		self.values = values # list of items.
		self.value = values[0]
	def access(self):
		return self.value
	def getValues(self):
		return self.values
	def __str__(self):
		from rltext import makeEnglishList
		return "chosen between %s" % ( makeEnglishList( [ str(v) for v in self.values ] ), )
#		return "chosen between %s, initially %s" % ( makeEnglishList( [ str(v) for v in self.values ] ), str(self.value) )
supportsSerialization( DialValue )

class SpectacularFailure (Exception): # when in doubt, explode
	def __init__(self, description):
		self.description = description

# diagnostics; identifying an error with a device (such that it cannot
# even be built) in a hopefully helpful way.
class PropertyNotSet:
	def __init__(self, component, property):
		self.component = component
		self.property = property

class InappropriateProperty:
	def __init__(self, component, property, issue = "invalid" ):
		self.component = component
		self.property = property
		self.issue = issue

from erlrays import Effect

class Component:
	serializedAttributes = [
		"label",
		"name",
		"property",
		"device",
		"componentId"
	]
	componentName = "Thingamajig"
	componentDescription = "A mystical-looking thingamajig."
	def __init__(self):
		self.label = "" # user might set this to distinguish between equal-type components more easily 
		self.name = self.componentName
		self.property = {}
	def initialize(self, property, propertyType, documentation = None, value = None):
		if not documentation:
			documentation = "you're not entirely sure what this is for"
		self.property[ property ] = [ value, propertyType, documentation ]
	def __setitem__(self, name, value):
		if not self.property.has_key( name ):
			raise InappropriateProperty()
		self.property[name][0] = value
	def __getitem__(self, name):
		if not self.property.has_key( name ):
			return None
		self.device.countcall()
		return self.property[name][0].access()
	def reset(self):
		pass
	def requestEffect(self):
		return None # return unique id of effect
	def requestEnergy(self, amount):
		return 0
	def requestObjects(self, amount):
		return None
	def signal(self, id):
		# id can be of any type, and is tested
		# with == and never displayed.
		# strings are used internally and so should
		# not be specifiable by the user (a workaround
		# could be checksums).
		# Suggested for user: integers ("frequencies")
		pass
	def tick(self):
		pass
	def mayLoad(self):
		return None
	# "interface" stuff is done with has_attr.
	# interface methods:
	#	.pullTrigger()
	#	.receiveRadio(frequency)
	def __str__(self):
		showId = True
		parts = []
		if showId and hasattr( self, "componentId" ):
			parts.append( "#%d" % self.componentId )
		if self.label:
			parts.append( "\"%s\"" % self.label )
		detDesc = self.detailDescription()
		if detDesc:
			parts.append( detDesc )
		if parts:
			return "%s (%s)" % ( self.name, ", ".join( parts ) )
		return self.name
	def baseDiagnostics(self):   	# this model is just because I find the Python
					# handling of "overloaded" methods in a subclass
					#confusing; I'm less likely to forget this way.
		issues = []
		for property in self.property.keys():
			if self.property[key] == None:
				issues.append( PropertyNotSet( self, property ) )
		return issues
	def detailDescription(self):
		return None
	def diagnostics(self): # sane if [] is returned
		return self.baseDiagnostics()
	def dials(self):
		return [ key for key in self.property.keys() if self.property[key][0].__class__ == DialValue ]
	def description(self):
		return str(self)
	def sensifiedSanity(self):
		for property in self.property.keys():
			if not self.property[property][0]:
				return False
		return True

# 

# for this to make sense, a device should be a unique, particular object,
# not an "item type". which means it should quack as an "ItemStack".

# PushableDevice should probably split this in two; with a Device
# parent class.

# Revelation: there is no need for more than these two.
# A "grenade" is a handheld device that is not too bulky to throw.
# There is no distinction between one-handed and two-handed devices.
# A "tank" is simply a pushable cart that has been given
# some sort of animation.

class IdGenerator:
	serializedAttributes = [
		"next"
	]
	def __init__(self):
		self.next = 0
	def __call__(self):
		self.next += 1
		return self.next
#nextDeviceId = IdGenerator()

class Device:
	serializedAttributes = [
		"componentIdGenerator",
		"name",
		"game",
		"components",
		"callLimit",
		"controller",
		"glyph",
		"chassis",
		"callcounter",
		"hostProvider",
		"ingredients",
		"deviceId",
		"mass",
		"bulk",
	]
	def __init__(self, game, name = None, glyph = Glyph( '?', stdDeviceColour ), container = None, components = []):
		self.componentIdGenerator = IdGenerator()
		self.game = game
		if not name:
			name = Noun( indefiniteArticle="a", singular="device" )
		self.name = name
		self.components = []
		for component in components:
			self.addComponent( component )
		self.callLimit = 900
		self.controller = None
		self.reset()
		# begin ItemStack duck code
		self.glyph = glyph
		self.chassis = None
	def setName(self, stringName):
		if stringName.upper().startswith("A "):
			article = "a"
			name = stringName[2:]
		elif stringName.upper().startswith("AN "):
			article = "an"
			name = stringName[3:]
		elif stringName.upper()[0] in "AEIOU":
			article = "an"
			name = stringName
		else:
			article = "a"
			name = stringName
		self.name = Noun( indefiniteArticle=article, singular=name )
	def setGlyphCharacter(self, character):
		self.glyph.char = character
	def addComponent(self, component):
		component.device = self
		component.componentId = self.componentIdGenerator()
		self.components.append( component )
	def removeComponent(self, component):
		self.components.remove( component )
		self.sensify()
	def startup(self):
		self.game.allDevices.append( self )
	def destroy(self):
		self.game.allDevices.remove( self )
	def reset(self):
		self.callcounter = 0
		for component in self.components:
			component.reset()
	def countcall(self):
		# really counts variable accesses, but there should be at
		# least one for every call.
		self.callcounter += 1
		if self.callcounter > self.callLimit:
			raise SpectacularFailure( "call limit exceeded" )
	def tick(self):
		for component in self.components:
			component.tick()
	def host(self):
		return self.hostProvider.host()
	def triggers(self):
		return [ component for component in self.components if hasattr( component, "pullTrigger" ) ]
	def componentsWithDials(self):
		return [ component for component in self.components if component.dials() ]
	def findComponent(self, componentId):
		for component in self.components:
			if component.componentId == componentId:
				return component
		return None
	def typelegal(self, pType, value):
		# mirrors are of the following form:
		# 	mirror:componentId:propertyName
		# a mirror interfae can NOT itself be mirrored.
		if value == None: return None # not legal, so replace with.. None.
		if pType.startswith( "mirror:" ):
			mirrorId, componentId, propertyName = pType.split(":")
			component = self.findComponent( componentId )
			if not component: return False
			try:
				propValue, propType, propDoc = component.property[ propertyName ]
			except KeyError:
				return False
			if propType.startswith("mirror:"): return False
			return self.typelegal( propType, value )
		if pType == "integer":
			return type(value) == int # long support is unlikely to be needed
		elif pType == "component":
			return value in self.components
		elif pType == "boolean":
			return type(value) == bool
		return False
	def propertyOkay(self, component, property):
		pValue, pType, pDoc = component.property[property]
		if pValue == None:
			return False
		for value in pValue.getValues():
			if not self.typelegal( pType, value ):
				return False # no mercy, wipe the whole property
		return True
	def sensify(self):
		# remove all variables that point to components or dials that no longer
		# exist, and all variables that are no longer type-legal (this can
		# happen with e.g. components that set dials)
		for component in self.components:
			for property in component.property.keys():
				if not self.propertyOkay( component, property ):
					component.property[property][0] = None
	def sanity(self):
		self.sensify()
		if not self.components:
			return False
		for component in self.components:
			if not component.sensifiedSanity():
				return False
		return True
	def spawn(self):
		import copy
		rv = copy.deepcopy( self ) # this line is honestly the root of all evil
		rv.deviceId = self.game.nextDeviceId()
		return rv
	def specifications(self):
		from math import sqrt, pow
		# no random elements -- this is so the weight projection is accurate.
		from erlcomponents import bulkTable, bulkToMaterialNeedTable, complexityTable
		totalBulk = int( 0.5 + sqrt( sum( [ bulkTable[component.bulk]*bulkTable[component.bulk] for component in self.components ] ) ) )
		totalMaterialNeeds = sum( [ bulkToMaterialNeedTable[component.bulk] for component in self.components ] )
		totalResourceNeeds = sum( [ complexityTable[component.complexity] for component in self.components ] )
		totalResourceNeeds += int( 0.5 + len( self.components ) ** 1.5 )
		totalWeight = totalMaterialNeeds + totalResourceNeeds * universalSprocketWeight
		for component in self.components:
			for specialIngredient in component.specialIngredients:
				totalWeight += specialIngredient.weight
			containerCapacity = getattr( component, "containerCapacity", None )
			containerItemType = getattr( component, "containerItemType", None )
			if containerCapacity and containerItemType:
				# For the purposes of weight, we assume all tanks are full.
				totalWeight += containerCapacity * containerItemType.weight
		return totalBulk, totalWeight, totalResourceNeeds, totalMaterialNeeds
	def finalize(self):
		from erlitem import ItemTable, Inventory
		bulk, weight, resources, materials = self.specifications()
		self.bulk = bulk
		self.mass = weight
		self.ingredients = Inventory()
		for component in self.components:
			for ingredient in component.specialIngredients:
				self.ingredients.add( ingredient.spawn() )
		for i in range( resources ):
			item = random.choice( ItemTable.RandomResources )
			self.ingredients.add( item.spawn() )
		while materials > 0:
			amount = random.randint( 1, 300 )
			item = random.choice( ItemTable.RandomMaterials )
			self.ingredients.add( item.spawn( amount ) )
			materials -= amount
	def create(self, creator):
		rv = self.chassis.shellClass( self.chassis, self )
		rv.created( creator )
		return rv
	def containers(self):
		return [ component for component in self.components if hasattr( component, "loadItems" ) ]

class DeviceInstance:
	def __init__(self, prototype):
		self.device = prototype.spawn()
	def reset(self):
		self.device.reset()
	def tick(self):
		try:
			self.device.tick()
		except SpectacularFailure:
			self.selfdestruct()
	def pullTrigger(self, component):
		assert component in self.device.components
		try:
			component.pullTrigger()
		except SpectacularFailure:
			self.selfdestruct()
	def dropDead(self, level, x, y):
		for ingredient in self.device.ingredients:
			amt = 0	
			for i in range( ingredient.amount ):
				amt += random.randint(0,1)
			if amt:
				drop = ingredient.itemType.spawn( amt )
				level.putItem( drop, x, y )
		

class HandheldDevice (DeviceInstance): # This will do the quacking; "Device" shall no longer be a duck of any kind
	serializedAttributes = [
		"itemType",
		"glyph",
		"amount",
		"container",
		"device",
		"controller",
		"x",
		"y",
		"facing",
		"level"
	]
	def __init__(self, chassis, prototype):
		DeviceInstance.__init__(self, prototype)
		self.itemType = ItemTypeUnique( self.device.name, self.device.glyph, self.device.mass )
		self.glyph = self.device.glyph
		self.amount = 1
		self.container = None
		self.device.hostProvider = self
		self.device.game.allDevices.append( self )
	def take(self, amount):
		if amount > 0:
			if self.container:
				self.container.remove( self )
			self.container = None
			return self
	def host(self):
		if self.controller:
			return self.controller
		return self # this 
	def triggers(self):
		return self.device.triggers()
	def componentsWithDials(self):
		return self.device.componentsWithDials()
	def commonName(self):
		if hasattr( self.device, "deviceId" ):
			return "%s (#%d)" % ( self.itemType.name.singular, self.device.deviceId )
		return self.itemType.name.singular
	def description(self):
		return "%s %s" % ( self.itemType.name.indefiniteArticle, self.commonName() )
	def created(self, creator):
		creator.giveItem( self )
	def sameItem(self, that):
		return id(self) == id(that)
	def weight(self):
		return self.device.mass
	def containers(self):
		return self.device.containers()
	def selfdestruct(self, violently = False ):
		self.device.game.allDevices.remove( self )
		if self.controller == self.device.game.at:
			self.device.game.at.readyItem( None, silent=True )
			if violently:
				self.device.game.interface.messages.add( "The %s explodes in your hands!" % self.commonName() )
			else:
				self.device.game.interface.messages.add( "The %s breaks down in your hands!" % self.commonName() )
				self.device.game.at.damage( randomRoll( 5 ) ) # a bit ironic. but the real damage is done elsewhere.
			x, y, level = self.device.game.at.x, self.device.game.at.y, self.device.game.at.level
		elif self.device.game.at.fov.isLit( self.x, self.y ):
			x, y, level = self.x, self.y, self.level
			if violently:
				self.device.game.interface.messages.add( "The %s explodes!" % self.commonName() )
			else:
				self.device.game.interface.messages.add( "The %s breaks down!" % self.commonName() )
		else:
			x, y, level = self.x, self.y, self.level
		self.take(1)
		if not violently:
			self.dropDead( level, x, y )
isAllowedPointer( HandheldDevice )
	

class Chassis:
	def __init__(self, shellClass, shortDescription, description, bulkLimit = None, weightLimit = None ):
		self.shellClass = shellClass
		self.description = description
		self.shortDescription = shortDescription
		self.bulkLimit = bulkLimit
		self.weightLimit = weightLimit

chassisHandheld = Chassis( HandheldDevice, "handheld", "A handheld device.", weightLimit = pcCarryingCapacity, bulkLimit = handheldBulkLimit)
